06 | 牛刀小试:如何搭建优惠券计算服务和用户服务?

讲述:姚秋辰

时长27:16大小24.98M

你好,我是姚秋辰。
上一节课我们搭建了 coupon-template-serv 模块,实现了优惠券模板的创建和批量查询等功能,相信你已经对如何使用 Spring Boot 搭建应用驾轻就熟了。今天我们就来搭建优惠券平台项目的另外两个模块,coupon-calculation-serv(优惠计算服务)和 coupon-customer-serv(用户服务),组建一个完整的实战项目应用(middleware 模块将在 Spring Cloud 环节进行搭建)。
通过今天的课程,你可以巩固并加深 Spring Boot 的实操能力,为接下来 Spring Cloud 微服务化改造打好前置知识的基础,在这节课里我也会分享一些关于设计模式和数据冗余的经验之谈。
另外,这节课的源码都可以在Gitee 代码库中找到。你可不要只读爽文不动手敲代码,我建议你把代码下载到本地,对照着源码动手练习一遍,才能学为己用。
闲话少叙,我们根据优惠券项目的依赖关系,先从上游服务 coupon-calculation-serv 开始动手搭建吧。

搭建 coupon-calculation-serv

coupon-calculation-serv 提供了用于计算订单的优惠信息的接口,它是一个典型的“计算密集型”服务。所谓计算密集型服务一般具备下面的两个特征:
不吃网络 IO 和磁盘空间
运行期主要占用 CPU、内存等计算资源
在做大型应用架构的时候,我们通常会把计算密集型服务与 IO/ 存储密集型服务分割开来,这样做的一个主要原因是提高资源利用率。
比如说,我们有一个计算密集型的微服务 A 和一个 IO 密集型微服务 B,大促峰值流量到来的时候,如果微服务 A 面临的压力比较大,我可以专门调配高性能 CPU 和内存等“计算类”的资源去定向扩容 A 集群;如果微服务 B 压力吃紧了,我可以定向调拨云上的存储资源分配给 B 集群,这样就实现了一种“按需分配”。
假如微服务 A 和微服务 B 合二为一变成了一个服务,那么在分配资源的时候就无法做到定向调拨,全链路压测环节也难以精准定位各项性能指标,这难免出现资源浪费的情况。这也是为什么,我要把优惠计算这个服务单独拿出来的原因。
现在,我们开始着手搭建 coupon-calculation-serv 下的子模块。和 coupon-template-serv 结构类似,coupon-calculation-serv 下面也分了若干个子模块,包括 API 层和业务逻辑层。API 层定义了公共的 POJO 类,业务逻辑层主要实现优惠价格计算业务。因为 calculation 服务并不需要访问数据库,所以没有 DAO 模块。
根据子模块间的依赖关系,我们就先从 coupon-calculation-api 这个接口层子模块开始搭建吧。

搭建 coupon-calculation-api

如果 coupon-calculation-serv 需要计算订单的优惠价格,那就得知道当前订单用了什么优惠券。封装了优惠券信息的 Java 类 CouponInfo 位于 coupon-template-api 包下,因此我们需要把 coupon-template-api 的依赖项加入到 coupon-calculation-api 中。
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>coupon-template-api</artifactId>
<version>${project.version}</version>
</dependency>
添加好了依赖项之后,接下来我们定义用于封装订单信息的 ShoppingCart 类。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ShoppingCart {
// 订单的商品列表 -
@NotEmpty
private List<Product> products;
// 封装了优惠券信息,目前计算服务只支持单张优惠券
// 为了考虑到以后多券的扩展性,所以定义成了List
private Long couponId;
private List<CouponInfo> couponInfos;
// 订单的最终价格
private long cost;
// 用户ID
@NotNull
private Long userId;
}
在上面的源码中,我们看到 ShoppingCart 订单类中使用了 Product 对象,来封装当前订单的商品列表。在 Product 类中包含了商品的单价、商品数量,以及当前商品的门店 ID。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
// 商品的价格
private long price;
// 商品在购物车里的数量
private Integer count;
// 商品销售的门店
private Long shopId;
}
在电商领域中,商品的数量通常不能以 Integer 整数来表示,这是因为只有标品才能以整数计件。对于像蔬菜、肉类等非标品来说,它们的计件单位并不是“个”。所以在实际项目中,尤其是零售行业的业务系统里,计件单位要允许小数位的存在。而我们的实战项目为了简化业务,就假定所有商品都是“标品”了。
在下单的时候,你可能有多张优惠券可供选择,你需要通过“价格试算”来模拟计算每张优惠券可以扣减的金额,进而选择最优惠的券来使用。SimulationOrder 和 SimulationResponse 分别代表了“价格试算”的订单类,以及返回的计算结果 Response。我们来看一下这两个类的源码。
// 优惠券价格试算
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SimulationOrder {
@NotEmpty
private List<Product> products;
@NotEmpty
private List<Long> couponIDs;
private List<CouponInfo> couponInfos;
@NotNull
private Long userId;
}
// 订单试算结果,可以看出哪个优惠券的优惠力度最大
@Data
@NoArgsConstructor
public class SimulationResponse {
// 最省钱的coupon
private Long bestCouponId;
// 每一个coupon对应的order价格
private Map<Long, Long> couponToOrderPrice = Maps.newHashMap();
}
到这里,coupon-calculation-api 模块就搭建好了。因为 calculation 服务不需要访问数据库,所以我们就不用搭建 dao 模块了,直接来实现 coupon-calculation-impl 业务层的代码逻辑。

搭建 coupon-calculation-impl

首先,我们在 coupon-calculation-impl 的 pom.xml 文件中添加下面的三个依赖项。
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>coupon-template-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>coupon-calculation-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
从 coupon-template-api 和 coupon-calculation-api 两个依赖项中,你可以拿到订单优惠计算过程用到的 POJO 对象。接下来,我们可以动手实现优惠计算逻辑了。
在搭建优惠计算业务逻辑的过程中,我运用了模板设计模式来封装计算逻辑。模板模式是一种基于抽象类的设计模式,它的思想很简单,就是将共性的算法骨架部分上升到抽象层,将个性部分延迟到子类中去实现
优惠券类型有很多种,比如满减券、打折券、随机立减等等,这些券的计算流程(共性部分)是相同的,但具体的计算规则(个性部分)是不同的。我将共性的部分抽象成了 AbstractRuleTemplate 抽象类,将各个券的差异性计算方式做成了抽象类的子类。
让我们看一下计算逻辑的类结构图。
在这张图里,顶层接口 RuleTemplate 定义了 calculate 方法,抽象模板类 AbstractRuleTemplate 将通用的模板计算逻辑在 calculate 方法中实现,同时它还定义了一个抽象方法 calculateNewPrice 作为子类的扩展点。各个具体的优惠计算类通过继承 AbstractRuleTemplate,并实现 calculateNewPrice 来编写自己的优惠计算方式。
我们先来看一下 AbstractRuleTemplate 抽象类的代码,走读 calculate 模板方法中的计算逻辑实现。
public ShoppingCart calculate(ShoppingCart order) {
// 获取订单总价
Long orderTotalAmount = getTotalPrice(order.getProducts());
// 获取以shopId为维度的总价统计
Map<Long, Long> sumAmount = getTotalPriceGroupByShop(order.getProducts());
CouponTemplateInfo template = order.getCouponInfos().get(0).getTemplate();
// 最低消费限制
Long threshold = template.getRule().getDiscount().getThreshold();
// 优惠金额或者打折比例
Long quota = template.getRule().getDiscount().getQuota();
// 如果优惠券未指定shopId,则shopTotalAmount=orderTotalAmount
// 如果指定了shopId,则shopTotalAmount=对应门店下商品总价
Long shopId = template.getShopId();
Long shopTotalAmount = (shopId == null) ? orderTotalAmount : sumAmount.get(shopId);
// 如果不符合优惠券使用标准, 则直接按原价走,不使用优惠券
if (shopTotalAmount == null || shopTotalAmount < threshold) {
log.debug("Totals of amount not meet");
order.setCost(orderTotalAmount);
order.setCouponInfos(Collections.emptyList());
return order;
}
// 子类中实现calculateNewPrice计算新的价格
Long newCost = calculateNewPrice(orderTotalAmount, shopTotalAmount, quota);
if (newCost < minCost()) {
newCost = minCost();
}
order.setCost(newCost);
log.debug("original price={}, new price={}", orderTotalAmount, newCost);
return order;
}
在上面的源码中,我们看到大部分计算逻辑都在抽象类中做了实现,子类只要实现 calculateNewPrice 方法完成属于自己的订单价格计算就好。我们以满减规则类为例来看一下它的实现。
@Slf4j
@Component
public class MoneyOffTemplate extends AbstractRuleTemplate implements RuleTemplate {
@Override
protected Long calculateNewPrice(Long totalAmount, Long shopAmount, Long quota) {
// benefitAmount是扣减的价格
// 如果当前门店的商品总价<quota,那么最多只能扣减shopAmount的钱数
Long benefitAmount = shopAmount < quota ? shopAmount : quota;
return totalAmount - benefitAmount;
}
}
在上面的源码中,我们看到子类业务的逻辑非常简单清爽。通过模板设计模式,我在抽象类中封装了共性逻辑,在子类中扩展了可变逻辑,每个子类只用关注自己的特定实现即可,使得代码逻辑变得更加清晰,大大降低了代码冗余。
随着业务发展,你的优惠券模板类型可能会进一步增加,比如赠品券、随机立减券等等,如果当前的抽象类无法满足新的需求,你可以通过建立多级抽象类的方式进一步增加抽象层次,不断将共性不变的部分抽取为抽象层。
创建完优惠计算逻辑,我们接下来看一下 Service 层的代码实现逻辑。Service 层的 calculateOrderPrice 代码非常简单,通过 CouponTemplateFactory 工厂类获取到具体的计算规则,然后调用 calculate 计算订单价格就好了。simulate 方法实现了订单价格试算,帮助用户在下单之前了解每个优惠券可以扣减的金额,从而选出最省钱的那个券。
@Slf4j
@Service
public class CouponCalculationServiceImpl implements CouponCalculationService {
// 优惠券结算
// 这里通过Factory类决定使用哪个底层Rule,底层规则对上层透明
@Override
public ShoppingCart calculateOrderPrice(@RequestBody ShoppingCart cart) {
log.info("calculate order price: {}", JSON.toJSONString(cart));
RuleTemplate ruleTemplate = couponTemplateFactory.getTemplate(cart);
return ruleTemplate.calculate(cart);
}
// 试计算每个优惠券在使用后订单的价格
// 页面上给用户提示最省钱的优惠券
@Override
public SimulationResponse simulate(@RequestBody SimulationOrder order) {
SimulationResponse response = new SimulationResponse();
Long minOrderPrice = Long.MIN_VALUE;
// 计算每一个优惠券的订单价格
for (CouponInfo coupon : order.getCouponInfos()) {
ShoppingCart cart = new ShoppingCart();
cart.setProducts(order.getProducts());
cart.setCouponInfos(Lists.newArrayList(coupon));
cart = couponProcessorFactory.getTemplate(cart).calculate(cart);
Long couponId = coupon.getId();
Long orderPrice = cart.getCost();
// 设置当前优惠券对应的订单价格
response.getCouponToOrderPrice().put(couponId, orderPrice);
// 比较订单价格,设置当前最优优惠券的ID
if (minOrderPrice > orderPrice) {
response.setBestCouponId(coupon.getId());
minOrderPrice = orderPrice;
}
}
return response;
}
// 其它方法未列出,请至源码仓库查看完整代码
}
在上面的源码中,我们看到,优惠券结算方法不用关心订单上使用的优惠券是满减券还是打折券,因为工厂方法会将子类转为顶层接口 RuleTemplate 返回。在写代码的过程中,我们也要有这样一种意识,就是尽可能对上层业务屏蔽其底层业务复杂度,底层具体业务逻辑的修改对上层是无感知的,这其实也是开闭原则的思想。
完成 Service 层后,我们接下来新建一个 CouponCalculationController 类,对外暴露 2 个 POST 接口,第一个接口完成订单优惠价格计算,第二个接口完成优惠券价格试算。
@Slf4j
@RestController
@RequestMapping("calculator")
public class CouponCalculationController {
@Autowired
private CouponCalculationService couponCalculationService;
// 优惠券结算
@PostMapping("/checkout")
@ResponseBody
public ShoppingCart calculateOrderPrice(@RequestBody ShoppingCart settlement) {
log.info("do calculation: {}", JSON.toJSONString(settlement));
return couponCalculationService.calculateOrderPrice(settlement);
}
// 优惠券列表挨个试算
// 给客户提示每个可用券的优惠额度,帮助挑选
@PostMapping("/simulate")
@ResponseBody
public SimulationResponse simulate(@RequestBody SimulationOrder order) {
log.info("do simulation: {}", JSON.toJSONString(order));
return couponCalculationService.simulateOrder(order);
}
// 其它方法未列出,请至源码仓库查看完整代码
}
好了,现在你已经完成了所有业务逻辑的源码。最后一步画龙点睛,你还需要为 coupon-calculation-impl 应用创建一个 Application 启动类并添加 application.yml 配置项。因为它并不需要访问数据库,所以你不需要在配置文件或者启动类注解上添加 spring-data 的相关内容。
到这里,我们就完成了优惠计算服务的搭建工作,你可以到我的代码仓库中查看完整的 coupon-calculation-serv 源码实现。
下面,我们去搭建优惠券项目的最后一个服务:coupon-customer-serv。

搭建 coupon-customer-serv

coupon-customer-serv 是一个服务于用户的子模块,它的结构和 coupon-template-serv 一样,包含了 API 层、DAO 层和业务逻辑层。它实现了用户领券、用户优惠券查找和订单结算功能。
为了简化业务逻辑,我在源码里省略了“用户注册”等业务功能,使用 userId 来表示一个已注册的用户。
按照惯例,我们先从 API 层开始搭建,搭建 coupon-customer-api 的过程非常简单。

搭建 coupon-customer-api

首先,我们需要把 coupon-template-api 和 coupon-calculation-api 这两个服务的依赖项添加到 coupon-customer-api 的 pom 依赖中,这样一来 customer 服务就可以引用到这两个服务的 Request 和 Response 对象了。
接下来,我们在 API 子模块中创建一个 RequestCoupon 类,作为用户领取优惠券的请求参数,通过传入用户 ID 和优惠券模板 ID,用户可以领取一张由指定模板打造的优惠券。另一个类是 SearchCoupon,用来封装优惠券查询的请求参数。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RequestCoupon {
// 用户领券
@NotNull
private Long userId;
// 券模板ID
@NotNull
private Long couponTemplateId;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SearchCoupon {
@NotNull
private Long userId;
private Long shopId;
private Integer couponStatus;
}
到这里,coupon-customer-api 就搭建完了。接下里我们去搭建 coupon-customer-dao 层,从数据层实现用户优惠券的增删改查。

搭建 coupon-customer-dao

我在 DAO 子模块中创建了一个 Coupon 数据库实体对象用于保存用户领到的优惠券,并按照 spring-data-jpa 规范创建了一个 CouponDAO 接口用来提供 CRUD 操作。
我们先来看一下 Coupon 实体对象的内容。
// 使用了lomkob注解自动生成建造者代码和getter、setter
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "coupon")
public class Coupon {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
// 对应的模板ID - 不使用one to one映射
@Column(name = "template_id", nullable = false)
private Long templateId;
// 拥有这张优惠券的用户的ID
@Column(name = "user_id", nullable = false)
private Long userId;
// 冗余一个shop id方便查找
@Column(name = "shop_id")
private Long shopId;
// 自动生成时间戳
@CreatedDate
@Column(name = "created_time", nullable = false)
private Date createdTime;
// CouponStatusConverter实现了AttributeConverter接口
// 将数据库value转化为CouponStatus类
@Column(name = "status", nullable = false)
@Convert(converter = CouponStatusConverter.class)
private CouponStatus status;
@Transient
private CouponTemplateInfo templateInfo;
}
在上面的源码中,我在 class 级别使用了 Lombok 注解自动生成代码,如果你对 Lomkob 比较感兴趣,可以从Lomkob 官网上获取更多的使用方法。
从这段代码引申一下,我想和你分享一个关于“数据冗余”的小知识点。我们看到 Coupon 实体对象中冗余保存了一个 Shop ID,之所以说它是冗余字段,是因为 Shop ID 可以从 CouponTemplate 表中获取,顺着 Coupon 对象的 templateID 字段可以关联到 CouponTemplate 表,进而获取到 ShopID 对象。
那我们为什么需要在 Coupon 表中再保存一次 shop ID 呢?如果严格遵循数据库的范式,那确实不应该保存一个冗余的 shop ID 字段,但我们也不要忘了,所谓范式和规则就是留给后人打破的。
数据库的标准范式是上一个时代的产物,以那个时代的眼光来看,“存储”是一项很宝贵的资源,在做程序设计的时候应该尽可能节省磁盘空间、内存空间,反倒“性能”和“高并发”并不是需要担心的事情。
当我们用现在的眼光来审视程序设计,你会发现“存储资源”已经不再是制约生产力的瓶颈,为了应对高并发的场景,你必须尽可能提高系统的吞吐量和性能
因此,你经常可以看到一二线大厂的高并发系统大量使用了“数据冗余”和“数据异构”方案。这是一个“以空间换时间”的路子,通过将一份数据冗余或异构到多处,提升业务的查询和处理效率。
了解了数据冗余的扩展知识后,我们来看下 DAO 层的接口类的内容:
public interface CouponDao extends JpaRepository<Coupon, Long> {
// 根据用户ID和Template ID,统计用户从当前优惠券模板中领了多少张券
long countByUserIdAndTemplateId(Long userId, Long templateId);
}
在上面的源码中,我们只创建了一个接口用于 count 计算,至于其他增删改查功能则统一由父类 JpaRepository 一手包办了。spring-data-jpa 沿袭了 spring 框架的简约风,大道至简解放双手,整个 Spring 框架从诞生至今,也一直都在朝着不断简化的方向发展。
到这里,coupon-customer-dao 层的代码就写完了,接下来我们去搞定最后一个子模块 coupon-customer-impl 业务逻辑层。

搭建 coupon-customer-impl

既然 coupon-customer-impl 需要调用 template 和 calculation 两个服务,在没有进入微服务化改造之前,我们只能先暂时委屈一下 template 和 calculation,将它俩作为 customer 服务的一部分,做成一个三合一的单体应用。等你学到微服务课程的时候,这个单体应用会被拆分成独立的微服务模块。
首先,你需要将 template、calculation 的依赖项添加到 coupon-customer-impl 的配置文件中,注意这里我们添加的可不是 API 接口层的依赖,而是 Impl 接口实现层的依赖。
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>coupon-customer-dao</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>coupon-calculation-impl</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>coupon-template-impl</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
添加完依赖项之后,我们就可以去动手实现业务逻辑层了。
CouponCustomerService 是业务逻辑层的接口抽象,我添加了几个方法,用来实现用户领券、查询优惠券、下单核销优惠券、优惠券试算等功能。
// 用户对接服务
public interface CouponCustomerService {
// 领券接口
Coupon requestCoupon(RequestCoupon request);
// 核销优惠券
ShoppingCart placeOrder( info);
// 优惠券金额试算
SimulationResponse simulateOrderPrice(SimulationOrder order);
// 用户删除优惠券
void deleteCoupon(Long userId, Long couponId);
// 查询用户优惠券
List<CouponInfo> findCoupon(SearchCoupon request);
// xxx其它方法请参考源码
}
这里,我以 placeOrder 方法为例,带你走读一下它的源码。如果你对其它方法的源码感兴趣,可以到Gitee 源码库中找到 Spring Boot 急速落地篇的 CouponCustomerServiceImpl 类,查看源代码。
placeOrder 方法实现了用户下单 + 优惠券核销的功能,我们来看一下它的实现逻辑。
@Override
@Transactional
public ShppingCart placeOrder(ShppingCart order) {
// 购物车为空,丢出异常
if (CollectionUtils.isEmpty(order.getProducts())) {
log.error("invalid check out request, order={}", order);
throw new IllegalArgumentException("cart is empty");
}
Coupon coupon = null;
if (order.getCouponId() != null) {
// 如果有优惠券就把它查出来,看是不是属于当前用户并且可用
Coupon example = Coupon.builder().userId(order.getUserId())
.id(order.getCouponId())
.status(CouponStatus.AVAILABLE)
.build();
coupon = couponDao.findAll(Example.of(example)).stream()
.findFirst()
// 如果当前用户查不到可用优惠券,就抛出错误
.orElseThrow(() -> new RuntimeException("Coupon not found"));
// 优惠券有了,再把它的券模板信息查出
// 券模板里的Discount规则会在稍后用于订单价格计算
CouponInfo couponInfo = CouponConverter.convertToCoupon(coupon);
couponInfo.setTemplate(templateService.loadTemplateInfo(coupon.getTemplateId()));
order.setCouponInfos(Lists.newArrayList(couponInfo));
}
// 调用calculation服务使用优惠后的订单价格
ShppingCart checkoutInfo = calculationService.calculateOrderPrice(order);
if (coupon != null) {
// 如果优惠券没有被结算掉,而用户传递了优惠券,报错提示该订单满足不了优惠条件
if (CollectionUtils.isEmpty(checkoutInfo.getCouponInfos())) {
log.error("cannot apply coupon to order, couponId={}", coupon.getId());
throw new IllegalArgumentException("coupon is not applicable to this order");
}
log.info("update coupon status to used, couponId={}", coupon.getId());
coupon.setStatus(CouponStatus.USED);
couponDao.save(coupon);
}
return checkoutInfo;
}
在上面的源码中,我们看到 Coupon 对象的构造使用了 Builder 链式编程的风格,这是得益于在 Coupon 类上面声明的 Lombok 的 Builder 注解,只用一个 Builder 注解就能享受链式构造的体验。
搞定了业务逻辑层后,接下来轮到 Controller 部分了,我在 CouponCustomerController 中对外暴露了几个服务,这些服务调用 CouponCustomerServiceImpl 中的方法实现各自的业务逻辑。
@Slf4j
@RestController
@RequestMapping("coupon-customer")
public class CouponCustomerController {
@Autowired
private CouponCustomerService customerService;
// ....省略部分方法,完整方法列表请参考源码
// 用户模拟计算每个优惠券的优惠价格
@PostMapping("simulateOrder")
public SimulationResponse simulate(@Valid @RequestBody SimulationOrder order) {
return customerService.simulateOrderPrice(order);
}
// 用户删除优惠券 - 非物理删除
@DeleteMapping("deleteCoupon")
public void deleteCoupon(@RequestParam("userId") Long userId,
@RequestParam("couponId") Long couponId) {
customerService.deleteCoupon(userId, couponId);
}
// 下单核销优惠券
@PostMapping("checkout")
public ShppingCart checkout(@Valid @RequestBody ShppingCart info) {
return customerService.placeOrder(info);
}
}
以上,就是所有的业务逻辑代码部分了。接下来你只需要完成启动类和配置文件,就可以启动项目做测试了。我先来带你看一下启动类的部分:
@SpringBootApplication
@EnableJpaAuditing
@ComponentScan(basePackages = {"com.geekbang"})
@EnableTransactionManagement
//用于扫描Dao @Repository
@EnableJpaRepositories(basePackages = {"com.geekbang"})
//用于扫描JPA实体类 @Entity,默认扫本包当下路径
@EntityScan(basePackages = {"com.geekbang"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
在上面的源码中,我们看到很多注解上都注明了 com.geekbang 作为包路径。之所以这么做,是因为 Spring Boot 的潜规则是将当前启动类类所在 package 作为扫包路径。
如果你的 Application 在 com.geekbang.customer 下,而你在项目中又需要加载来自 com.geekbang.template 下的类资源,就必须额外声明扫包路径,否则只有在 com.geekbang.customer 和其子路径之下的资源才会被加载。
关于配置项的部分,你可以直接把 coupon-template-impl 的配置文件 application.yml 照搬过来,不过,要记得把里面配置的 spring.application.name 改成 coupon-customer-serv
好,到这里,我们优惠券平台项目的 Spring Boot 版本就搭建完成了。现在,coupon-customer-serv 已经成了一个三合一的单体应用,你只要在本地启动这一个应用,就可以调用 customer、template 和 calculation 三个服务的功能。

总结

现在,我们来回顾一下这两节 Spring Boot 实战课的重点内容。通过这两节课,我带你搭建了完整的 Spring Boot 版优惠券平台的三个子模块。为了让项目结构更加清晰,我用分层设计的思想将每个模块拆分成 API 层、DAO 层和业务层。在搭建过程中,我们使用 spring-data-jpa 搞定了数据层,短短几行代码就能实现复杂的 CRUD 操作;使用 spring-web 搭建了 Controller 层,对外暴露了 RESTFul 风格的接口。
我们学习技术也分为外功修为和内功修行,讲究的是内外兼修。技术框架总会不断推陈出新,学会怎么使用一门技术,这修习的是外功。你掌握了一个功能强大的新框架,外功招式自然凌厉几分。但是能决定你武力值的上限有多高,还要靠你在工作学习中不断提高内功修为。
外功见效快而内功需要长期磨炼,就像我这节课分享的设计模式一样,设计模式就是典型的内功心法,学会一两种设计模式不会让你的技术水平产生突飞猛进的提高,但是当你逐渐融会贯通把各种设计模式活学活用到代码中,境界层次就变得不一样了。
从下一节课开始,我们将进入 Spring Cloud 基础篇的学习,通过基础篇的学习,你将熟练使用 Nacos、Loadbalancer 和 OpenFeign 组件来搭建基于微服务架构的跨服务调用。

思考题

如果我们分别把 coupon-customer-serv、coupon-template-serv 和 coupon-calculation-serv 分别部署在集群 A、B 和 C 上,你能想到几种方式,使得这几个应用可以在集群环境中互相发起调用呢?
我给你一个小提示,在思考这个问题的时候,你要想到一点,服务有可能会发生上下线而且集群也可能会扩容,要尽可能让调用请求发到正常工作的机器上,提高请求成功率。欢迎你在留言区分享你的想法和收获,我在留言区等你。
好啦,这节课就结束啦。也欢迎你把这节课分享给更多对 Spring Cloud 感兴趣的朋友。我是姚秋辰,我们下节课再见!
分享给需要的人,Ta订阅后你可得20现金奖励
生成海报并分享

赞 1

提建议

上一篇
05 | 牛刀小试:如何搭建优惠券模板服务?
 写留言

精选留言(7)

  • 深海🐟
    2021-12-24
    gitee上面的代码并不能直接run起来,建议先把数据库改成h2 然后放入建表语句等 直接跑起来再对照着看会好一点

    作者回复: 需要在本地先把数据库表创建好,把jdbc url和db用户名密码等配置好之后才能运行。改成H2先本地启动玩一玩是不错的方法

    共 3 条评论
    2
  • 暮雨yl晨曦
    2021-12-24
    思考题我能想到的方式也和Layne同学一样。
    想不出有比这3个更好的方式。
    在老师的代码中发现了一些问题。
    1.CouponCalculationService和它的实现类里的@RequestBody应该是可以去掉的。这个service类是放在controller package下的,是特地这么处理的吗?我的习惯一直都是controller和service是同一层级的package。另外,我的习惯是先有interface再有impl。也就是service package下都是interface,把ServiceImpl放到service.impl下。老师的代码里是service下放的是ServiceImpl,interface放在service.inf下。
    2.CouponCalculationController下的两个restful接口,应该少了@Valid注解。
    3.AntiPauTemplate,应该是拼错了。。是Pua。。。
    4.可以看出来老师已经工作非常非常多年了。用的还是比较老的guava的方式,其实现在可以不用了。比如Lists.newArrayList(),现在我们习惯用Collections.emptyList();
    coupons.stream().forEach(e -> e.setTemplateInfo(templateMap.get(e.getTemplateId())));
    直接:coupons.forEach(e -> e.setTemplateInfo(templateMap.get(e.getTemplateId())));这样就可以了。
    5.老师的注释喜欢用//,我还是喜欢用java doc的风格,就是/* */这种。不然idea一直提示也很难受。
    6.从代码细节上来看,我们所在的组写的代码在细节上比老师更优雅一些。这样我就放心了,多对比一下,看看自己哪里做的好哪里做的不好。老师的整体思路是很清晰的,多向老师学习。目前看起来我细节上处理还ok,但是对整个技术架构以及中间件没有非常深入了解,之后需要多花时间在这上面。
    7.我们这个课程看起来应该是以实战为主,原理性的东西会少一些。希望老师也能多提一些重要的原理性的点,以及面试经常被问到的知识点。
    8.顺带提一下,在有个章节,按照老师的方法安装rabbit mq,装完之后启动不起来,提示找不到命令。后来自己查了一下解决了。我猜测是系统版本引起的,我习惯把mac os升级到最新版。
    展开
    共 1 条评论
  • Layne
    2021-12-24
    回答下思考题:
    1.利用注册中心的方式;
    2.利用API网关的方式;
    3.利用类似nginx这种做VIP的负载均衡方式。

    作者回复: 非常不错,使用nginx、keepalive做vip是挺常用的方案,省钱又简单

  • kimoti
    2021-12-24
    修炼外功就是写业务逻辑代码,修炼内功就是算法,计算机底层原理,操作系统……

    作者回复: 每到面试的时候,内功心法都会大幅提升

  • peter
    2021-12-24
    请教两个问题:问题1:jpa本身不是具体的ORM框架,而是对ORM框架的一个封装,对吗?问题2:jpa只能用hibernate吗?能用mybatis吗?

    作者回复: orm可以理解为一个“理念”或者“口号”,在关系型DB和对象之间构建一层映射。jpa是一种“ORM规范”,它没有实现ORM,只是定义了一组持久化API的规范,我在项目中用的spring官方推荐的spring-data-jpa(底层hibernate实现)才是jpa规范的具体实现,像openjpa、toplink这些都是jpa的实现。mybatis你可以理解为半自动或者手动挡的ORM框架,它本身并不是jpa的实现,你可以了解一下mybatis-plus,比mybatis好用一些。

  • 时光@1
    2021-12-24
    老师 请问下 怎么样算是上游服务?

    作者回复: 就像一条河流一样,用户调用开始的地方是起始点,在最上游,然后逐渐流到了下游的底层服务。或者可以这么理解,你的服务所调用的服务,对于你的服务本身来说就是下游服务

    共 2 条评论
  • BOB
    2021-12-24
    服务启动后向注册中心注册服务地址(ip:port),其他服务调用前从注册中心获取最新的服务列表。注册中心维护各个服务的心跳,超时就把对应的服务地址移除,并将下线信息通知到其他订阅了这个服务的服务。服务间的调用失败时会换一个地址重试。

    作者回复: 同学你已经把我后面课程的内容总结好了

    共 2 条评论